package nf.util;

import java.util.Arrays;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.zip.Adler32;
import java.lang.reflect.*;

public class ArgumentParser
{
	private Map<Character, Option> shortOptions;
	private Map<String, Option> longOptions;
	private List<Option> options;
	
	public ArgumentParser()
	{
		shortOptions = new HashMap<Character, Option>();
		longOptions = new HashMap<String, Option>();
		options = new LinkedList<Option>();
	}
	
	public Option addOption(String key, String... parts)
	{
		Option opt = new Option(key, parts);
		char shortName;
		String longName;
		
		if (0 != (shortName = opt.getShortName())) {
			shortOptions.put(new Character(shortName), opt);
		}
		if (null != (longName = opt.getLongName())) {
			longOptions.put(longName, opt);
		}
		options.add(opt);
		
		return opt;
	}
	
	public Result parse(String... arguments) throws ArgumentParseException
	{
		Map<String, Object> opts = new HashMap<String, Object>(options.size());
		String[] args;
		
		// set the default values for any options that have them
		for (Option opt : options) {
			Object val = opt.getDefaultValue();
			if (val != null)
				opts.put(opt.getKey(), val);
		}
		
		int i;
		for (i = 0; i < arguments.length; i++) {
			String arg = arguments[i];
			Option opt;
			if (arg.charAt(0) == '-') {
				if (arg.charAt(1) == '-') {
					if (arg.length() == 2) {
						// explicit end of options
						i++;
						break;
					}
					
					// long option
					String[] parts = arg.substring(2).split("=", 2);
					opt = longOptions.get(parts[0]);
					if (opt == null) {
						throw new ArgumentParseException(null,
							"No such option --" + parts[0] + ".");
					}
					
					Object value = (parts.length > 1)
						? opt.parseValue(parts[1])
						: opt.getImplicitValue();
					opts.put(opt.getKey(), value);
				} else {
					char[] localOptions = arg.toCharArray();
					int end = localOptions.length - 1;
					for (int j = 1; j < localOptions.length; j++) {
						char next = localOptions[j];
						opt = shortOptions.get(next);
						if (opt == null) {
							throw new ArgumentParseException(null,
								"No such option -" + next + ".");
						}
						
						if (j == end && i < arguments.length - 1) {
							if (opt.expectsValue()) {
								opts.put(opt.getKey(),
									opt.parseValue(arguments[++i]));
							} else {
								opts.put(opt.getKey(), opt.getImplicitValue());
							}
						} else {
							opts.put(opt.getKey(), opt.getImplicitValue());
						}
					}
				}
			} else {
				// implicit end of options
				break;
			}
		}
		
		if (i < arguments.length) {
			int args_length = arguments.length - i;
			args = new String[args_length];
			for (int j = 0; j < args_length; j++) {
				args[j] = arguments[i + j];
			}
		} else {
			args = new String[0];
		}
		
		return new Result(opts, args);
	}
	
	public static class Result
	{
		public Map<String, ?> options;
		public String[] args;
		
		Result(Map<String, ?> options, String[] args)
		{
			this.options = options;
			this.args = args;
		}
		
		public String toString()
		{
			return String.format("<%s %s>", options, args);
		}
	}
	
	public static class Option
	{
		private String key;
		private char shortName;
		private String longName;
		private String description;
		
		private Mapper<String,?> valueMapper;
		private Object defaultValue;
		private Object implicitValue;
		
		Option(String key, String... parts)
		{
			if (key == null) {
				throw new IllegalArgumentException("The option key may not " +
					"be null.");
			}
			
			shortName = 0;
			this.key = key;
			
			for (String part : parts) {
				if (part.charAt(0) == '-') {
					parseSpecifier(part);
				} else {
					description = part;
				}
			}

			
			if (shortName == 0 && longName == null) {
				throw new IllegalArgumentException("Must provide either a " +
					"short or a long name for the option.");
			}
		}
		
		public String getKey()
		{
			return key;
		}
		
		public char getShortName()
		{
			return shortName;
		}
		
		public String getLongName()
		{
			return longName;
		}
		
		public String getDescription()
		{
			return description;
		}
		
		public Object getDefaultValue()
		{
			return defaultValue;
		}
		
		public Option setDefaultValue(boolean value)
		{
			setDefaultValue((value) ? Boolean.TRUE : Boolean.FALSE);
			return this;
		}
		
		public Option setDefaultValue(Object newDefault)
		{
			defaultValue = newDefault;
			return this;
		}
		
		public Object getImplicitValue()
		{
			return (implicitValue != null)
				? implicitValue
				: defaultValue;
		}
		
		public Option setImplicitValue(boolean value)
		{
			setImplicitValue((value) ? Boolean.TRUE : Boolean.FALSE);
			return this;
		}
		
		public Option setImplicitValue(Object newValue)
		{
			implicitValue = newValue;
			return this;
		}
		
		public boolean expectsValue()
		{
			return valueMapper != null;
		}
		
		public Option setMapper(Mapper<String, ?> mapper)
		{
			valueMapper = mapper;
			return this;
		}
		
		public Option mapToBoolean()
		{
			return mapToBoolean(true);
		}
		
		public Option mapToBoolean(boolean desiredValue)
		{
			final Boolean value = (desiredValue) ? Boolean.TRUE : Boolean.FALSE;
		
			valueMapper = new Mapper<String, Boolean>() {
				public Boolean map(String unnecessary)
				{
					return value;
				}
			};
			return this;
		}
		
		public Option setValueType(Class<?> valueClass)
		{
			Constructor<?> constructor;
			
			try {
				constructor = valueClass.getConstructor(String.class);
			} catch (NoSuchMethodException e) {
				try {
					constructor = valueClass.getConstructor(CharSequence.class);
				} catch (NoSuchMethodException e2) {
					throw new IllegalArgumentException(valueClass +
						" does not have a constructor that accepts a String.");
				}
			}
			
			class ClassMapper implements Mapper<String, Object>
			{
				private Constructor<?> constructor;
				
				public ClassMapper(Constructor<?> constructor)
				{
					this.constructor = constructor;
				}
				
				public Object map(String value) {
					try {
						return constructor.newInstance(value);
					} catch (InvocationTargetException e) {
						throw new IllegalArgumentException(e);
					} catch (IllegalAccessException e) {
						throw new RuntimeException(e);
					} catch (InstantiationException e) {
						throw new RuntimeException(e);
					}
				}
			}
			
			valueMapper = new ClassMapper(constructor);
			return this;
		}
		
		Object parseValue(String value) throws ArgumentParseException
		{
			if (valueMapper == null) {
				throw new ArgumentParseException(this,
					"The " + this + " option does not expect a value.");
			}
			
			try {
				return valueMapper.map(value);
			} catch (IllegalArgumentException e) {
				try {
					InvocationTargetException ite =
						(InvocationTargetException) e.getCause();
					throw new ArgumentParseException(this,
						"Error parsing value for " + this + ".",
						ite.getCause());
				} catch (ClassCastException cce) {
					throw new ArgumentParseException(this,
						"Error parsing value for " + this + ".", e);
				}
			}
		}
		
		public String toString()
		{
			if (longName != null) {
				return longName;
			} else {
				assert shortName > 0;
				char[] name = new char[2];
				name[0] = '-';
				name[1] = shortName;
				return new String(name);
			}
		}
		
		public int hashCode()
		{
			if (shortName > 0) {
				return (int) shortName;
			} else {
				assert longName != null;
				Adler32 checker = new Adler32();
				checker.update(longName.getBytes());
				return (int) checker.getValue();
			}
		}	
		
		private void parseSpecifier(String part)
		{
			try {
				if (part.charAt(1) == '-') {
					// long option name
					longName = part.substring(2);
				} else {
					// short option name
					if (part.length() != 2) {
						throw new IllegalArgumentException("Short option " +
							"specifiers must be exactly two characters long.");
					}
					shortName = part.charAt(1);
				}
			} catch (IndexOutOfBoundsException e) {
				throw new IllegalArgumentException("Incomplete specifier.");
			}
		}
	}
	
	public static void main(String... args)
	{
		ArgumentParser p = new ArgumentParser();
		p.addOption("force", "--force").setDefaultValue(false).setImplicitValue(true);
		p.addOption("test", "-n").setDefaultValue(new Integer(0)).setValueType(Integer.class);
		
		try {
			ArgumentParser.Result r = p.parse(args);
			System.out.println(r);
		} catch (ArgumentParseException e) {
			e.printStackTrace();
		}
	}
}
